
/*
 * Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is
 * subject to license terms.
 */ 

package org.jdesktop.application;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JTextArea;
import javax.swing.RootPaneContainer;
import javax.swing.Timer;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;



final class DefaultInputBlocker extends Task.InputBlocker {
    private static final Logger logger = Logger.getLogger(DefaultInputBlocker.class.getName());
    private JDialog modalDialog = null;

    DefaultInputBlocker(Task task, Task.BlockingScope scope, Object target, ApplicationAction action) {
        super(task, scope, target, action);
    }

    private void setActionTargetBlocked(boolean f) {
        javax.swing.Action action = (javax.swing.Action)getTarget();
        action.setEnabled(!f);
    }

    private void setComponentTargetBlocked(boolean f) {
        Component c = (Component)getTarget();
        c.setEnabled(!f);
        // Note: can't set the cursor on a disabled component
    }

    /* Accumulates a list of all of the descendants of root whose name 
     * begins with "BlockingDialog"
     */
    private void blockingDialogComponents(Component root, List<Component> rv) {
        String rootName = root.getName();
        if ((rootName != null) && rootName.startsWith("BlockingDialog")) {
            rv.add(root);
        }
        if (root instanceof Container) {
            for(Component child : ((Container)root).getComponents()) {
                blockingDialogComponents(child, rv);
            }
        }
    }
    private List<Component> blockingDialogComponents(Component root) {
        List<Component> rv = new ArrayList<Component>();
        blockingDialogComponents(root, rv);
        return rv;
    }

    /* Inject resources from both the Task's ResourceMap and the
     * ApplicationAction's ResourceMap.  We add the action's name
     * prefix to all of the components before the second step.
     */
    private void injectBlockingDialogComponents(Component root) {
        ResourceMap taskResourceMap = getTask().getResourceMap();
        if (taskResourceMap != null) {
            taskResourceMap.injectComponents(root);
        }
        ApplicationAction action = getAction();
        if (action != null) {
            ResourceMap actionResourceMap = action.getResourceMap();
            String actionName = action.getName();
            for(Component c : blockingDialogComponents(root)) {
                c.setName(actionName + "." + c.getName());
            }
            actionResourceMap.injectComponents(root);            
        }
    }

    /* Creates a dialog whose visuals are initialized from the 
     * following Task resources:
     * BlockingDialog.title
     * BlockingDialog.optionPane.icon
     * BlockingDialog.optionPane.message
     * BlockingDialog.cancelButton.text
     * BlockingDialog.cancelButton.icon
     * BlockingDialog.progressBar.stringPainted
     * 
     * If the Task has an Action then use the actionName as a prefix
     * and look up the resources again, in the action's ResourceMap
     * (that's the @Action's ApplicationActionMap ResourceMap really):
     * actionName.BlockingDialog.title
     * actionName.BlockingDialog.optionPane.icon
     * actionName.BlockingDialog.optionPane.message
     * actionName.BlockingDialog.cancelButton.text
     * actionName.BlockingDialog.cancelButton.icon
     * actionName.BlockingDialog.progressBar.stringPainted
     */
    private JDialog createBlockingDialog() {
        JOptionPane optionPane = new JOptionPane();
        /* If the task can be canceled, then add the cancel
         * button.  Otherwise clear the default OK button.
         */
        if (getTask().getUserCanCancel()) {
            JButton cancelButton = new JButton();
            cancelButton.setName("BlockingDialog.cancelButton");
            ActionListener doCancelTask = new ActionListener() {
                    public void actionPerformed(ActionEvent ignore) {
                        getTask().cancel(true);
                    }
                };
            cancelButton.addActionListener(doCancelTask);
            optionPane.setOptions(new Object[]{cancelButton});
        }
        else {
            optionPane.setOptions(new Object[]{}); // no OK button
        }
        /* Create the JDialog.  If the task can be canceled, then 
         * map closing the dialog window to canceling the task.
         */
        Component dialogOwner = (Component)getTarget();
        String taskTitle = getTask().getTitle();
        String dialogTitle = (taskTitle == null) ? "BlockingDialog" : taskTitle;
        final JDialog dialog = optionPane.createDialog(dialogOwner, dialogTitle);
        dialog.setModal(true);
        dialog.setName("BlockingDialog");
        dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
        WindowListener dialogCloseListener = new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                if (getTask().getUserCanCancel()) {
                    getTask().cancel(true);
                    dialog.setVisible(false);
                }
            }
        };
        dialog.addWindowListener(dialogCloseListener);
        optionPane.setName("BlockingDialog.optionPane");
        injectBlockingDialogComponents(dialog);
        /* Reset the JOptionPane's message property after injecting
         * an initial value for the message string.
         */
        recreateOptionPaneMessage(optionPane);
        dialog.pack();
        return dialog;
    }

    /* Replace the default message panel with one that where the
     * message text can be selected and that includes a status bar for
     * task progress.  We inject resources here because the 
     * JOptionPane#setMessage() doesn't add the panel to the JOptionPane
     * immediately.
     */
    private void recreateOptionPaneMessage(JOptionPane optionPane) {
        Object message = optionPane.getMessage();
        if (message instanceof String) {
            Font font = optionPane.getFont();
            final JTextArea textArea = new JTextArea((String)message);
            textArea.setFont(font);
            int lh = textArea.getFontMetrics(font).getHeight();
            Insets margin = new Insets(0, 0, lh, 24); // top left bottom right
            textArea.setMargin(margin);
            textArea.setEditable(false);
            textArea.setWrapStyleWord(true);
            textArea.setBackground(optionPane.getBackground());
            JPanel panel = new JPanel(new BorderLayout());
            panel.add(textArea, BorderLayout.CENTER);
            final JProgressBar progressBar = new JProgressBar();
            progressBar.setName("BlockingDialog.progressBar");
            progressBar.setIndeterminate(true);
            PropertyChangeListener taskPCL = new PropertyChangeListener() {
                public void propertyChange(PropertyChangeEvent e) {
                    if ("progress".equals(e.getPropertyName())) {
                        progressBar.setIndeterminate(false);
                        progressBar.setValue((Integer)e.getNewValue());
                        updateStatusBarString(progressBar);
                    }
                    else if ("message".equals(e.getPropertyName())) {
                        textArea.setText((String)e.getNewValue());
                    }
                }
            };
            getTask().addPropertyChangeListener(taskPCL);
            panel.add(progressBar, BorderLayout.SOUTH);
            injectBlockingDialogComponents(panel);
            optionPane.setMessage(panel);
        }
    }
    
    private void updateStatusBarString(JProgressBar progressBar) {
        if (!progressBar.isStringPainted()) {
            return;
        }
        /* The initial value of the progressBar string is the format.
         * We save the format string in a client property.  The format
         * String will be applied four values (see below).  The default 
         * format String is in resources/Application.properties, it's:
         * "%02d:%02d, %02d:%02d remaining"
         */
        String key = "progressBarStringFormat";
        if (progressBar.getClientProperty(key) == null) {
            progressBar.putClientProperty(key, progressBar.getString());
        }
        String fmt = (String)progressBar.getClientProperty(key);
        if  (progressBar.getValue() <= 0) {
            progressBar.setString("");
        }
        else if (fmt == null) {
            progressBar.setString(null);
        }
        else {
            double pctComplete = progressBar.getValue() / 100.0;
            long durSeconds = getTask().getExecutionDuration(TimeUnit.SECONDS);
            long durMinutes = durSeconds / 60;
            long remSeconds = (long)(0.5 + ((double)durSeconds / pctComplete)) - durSeconds;
            long remMinutes = remSeconds / 60;
            String s = String.format(fmt, durMinutes, durSeconds - (durMinutes * 60), 
                                          remMinutes, remSeconds - (remMinutes * 60));
            progressBar.setString(s);
        }
        
    }

    private void showBusyGlassPane(boolean f) {
        RootPaneContainer rpc = null;
        Component root = (Component)getTarget();
        while(root != null) {
            if (root instanceof RootPaneContainer) {
                rpc = (RootPaneContainer)root;
                break;
            }
            root = root.getParent();
        }
        if (rpc != null) {
            if (f) {
                JMenuBar menuBar = rpc.getRootPane().getJMenuBar();
                if (menuBar != null) {
                    menuBar.putClientProperty(this, menuBar.isEnabled());
                    menuBar.setEnabled(false);
                }
                JComponent glassPane = new BusyGlassPane();
                InputVerifier retainFocusWhileVisible = new InputVerifier() { 
                    public boolean verify(JComponent c) { 
                        return !c.isVisible(); 
                    } 
                };
                glassPane.setInputVerifier(retainFocusWhileVisible);
                Component oldGlassPane = rpc.getGlassPane();
                rpc.getRootPane().putClientProperty(this, oldGlassPane);
                rpc.setGlassPane(glassPane);
                glassPane.setVisible(true);
                glassPane.revalidate();
            }
            else {
                JMenuBar menuBar = rpc.getRootPane().getJMenuBar();
                if (menuBar != null) {
                    boolean enabled = (Boolean)menuBar.getClientProperty(this);
                    menuBar.putClientProperty(this, null);
                    menuBar.setEnabled(enabled);
                }
                Component oldGlassPane = (Component)rpc.getRootPane().getClientProperty(this);
                rpc.getRootPane().putClientProperty(this, null);
                if (!oldGlassPane.isVisible()) {
                    rpc.getGlassPane().setVisible(false);
                }
                rpc.setGlassPane(oldGlassPane); // sets oldGlassPane.visible
            }
        }
    }

    /* Note: unfortunately, the busy cursor is reset when the modal 
     * dialog is shown.
     */
    private static class BusyGlassPane extends JPanel  {
        BusyGlassPane() {
            super(null, false); 
            setVisible(false);
            setOpaque(false);
            setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
            MouseInputListener blockMouseEvents = new MouseInputAdapter() {};
            addMouseMotionListener(blockMouseEvents);
            addMouseListener(blockMouseEvents);
        }
    }

    /* If an action was specified then return the value of the 
     * actionName.BlockingDialogTimer.delay resource from the action's
     * resourceMap.  Otherwise return the value of the 
     * BlockingDialogTimer.delay resource from the Task's ResourceMap.
     * The latter's default in defined in resources/Application.properties.
     */
    private int blockingDialogDelay() {
        Integer delay = null;
        String key = "BlockingDialogTimer.delay";
        ApplicationAction action = getAction();
        if (action != null) {
            ResourceMap actionResourceMap = action.getResourceMap();
            String actionName = action.getName();
            delay = actionResourceMap.getInteger(actionName + "." + key);
        }
        ResourceMap taskResourceMap = getTask().getResourceMap();
        if ((delay == null) && (taskResourceMap != null)) {
            delay = taskResourceMap.getInteger(key);
        }
        return (delay == null) ? 0 : delay.intValue();
    }

    private void showBlockingDialog(boolean f) {
        if (f) {
            if (modalDialog != null) {
                String msg = String.format("unexpected InputBlocker state [%s] %s", f, this);
                logger.warning(msg);
                modalDialog.dispose();
            }
            modalDialog = createBlockingDialog();
            ActionListener showModalDialog = new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if (modalDialog != null) { // already dismissed
                        modalDialog.setVisible(true);
                    }
                }
            };
            Timer showModalDialogTimer = new Timer(blockingDialogDelay(), showModalDialog);
            showModalDialogTimer.setRepeats(false);
            showModalDialogTimer.start();
        }
        else {
            if (modalDialog != null) {
                modalDialog.dispose();
                modalDialog = null;
            }
            else {
                String msg = String.format("unexpected InputBlocker state [%s] %s", f, this);
                logger.warning(msg);
            }
        }
    }

    @Override protected void block() {
        switch (getScope()) {
        case ACTION:      
            setActionTargetBlocked(true); 
            break;
        case COMPONENT:   
            setComponentTargetBlocked(true); 
            break;
        case WINDOW:      
        case APPLICATION: 
            showBusyGlassPane(true);
            showBlockingDialog(true); 
            break;
        }
    }

    @Override protected void unblock() {
        switch (getScope()) {
        case ACTION:      
            setActionTargetBlocked(false); 
            break;
        case COMPONENT:   
            setComponentTargetBlocked(false); 
            break;
        case WINDOW:      
        case APPLICATION: 
            showBusyGlassPane(false);
            showBlockingDialog(false);
            break;
        }
    }
}
